/*
 * Copyright (c) 2009 DeledImporter
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'Pyro Games' nor the names of its contributors 
 *   may be used to endorse or promote products derived from this software 
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package net.pyrogames.jme;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;

import com.jme.bounding.BoundingBox;
import com.jme.image.Texture;
import com.jme.light.DirectionalLight;
import com.jme.light.Light;
import com.jme.light.PointLight;
import com.jme.light.SpotLight;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.scene.Node;
import com.jme.scene.TexCoords;
import com.jme.scene.TriMesh;
import com.jme.scene.state.BlendState;
import com.jme.scene.state.LightState;
import com.jme.scene.state.MaterialState;
import com.jme.scene.state.RenderState;
import com.jme.scene.state.TextureState;
import com.jme.system.DisplaySystem;
import com.jme.util.TextureManager;
import com.jme.util.export.binary.BinaryExporter;

import net.pyrogames.deled.geometry.DXSNode;
import net.pyrogames.deled.geometry.DXSPrimitive;
import net.pyrogames.deled.geometry.DXSSpatial;
import net.pyrogames.deled.geometry.DXSPrimitive.PolygonWind;
import net.pyrogames.deled.importer.Format;
import net.pyrogames.deled.importer.ImportException;
import net.pyrogames.deled.light.DXSDirectionalLight;
import net.pyrogames.deled.light.DXSLight;
import net.pyrogames.deled.light.DXSPointLight;
import net.pyrogames.deled.light.DXSSpotLight;
import net.pyrogames.deled.material.DXSColorRGBA;
import net.pyrogames.deled.material.DXSLayer;
import net.pyrogames.deled.material.DXSMaterial;
import net.pyrogames.deled.math.DXSVector3f;

/**
 * Default format for the jMonkey 2.0 3D engine. This is
 * intended primarily for simple models, as the DXS representation
 * is matched spatial to spatial and all lights are attached to
 * the root node of the model. Custom elements are ignored if present.
 * 
 * This format should be extended if you use extensions with the
 * <code>{@link DXSImporter}</code> for custom scene elements or
 * even if you want to adjust the scene hierarchy (e.g. have lights
 * be associated with specific nodes to allow for more lights in a scene).
 * 
 * @author Nicholas Woodfield
 *
 */
public class JME2Format implements Format<Node>{
	/** The display system for jme, this is just here for convienance */
	protected DisplaySystem display;
	/** Scale value to be applied to the whole model. 1.0 is default */
	protected float scale;
	/** Root of the scene node, the DXSImporter names this as the name of the dxs file */
	protected Node rootNode;
	/** URL pointing to the texture directory specified, if not specified, the default texture is used */
	protected URL texDir;
	
	/**
	 * Default constructor.
	 */
	public JME2Format(){
		display = DisplaySystem.getDisplaySystem();
		scale = 1.0f;
		texDir = null;
	}

	@Override
	public void loadData(DXSNode root) throws ImportException {
		rootNode = convertDXSNode(root);
		LightState state = display.getRenderer().createLightState();
		state.setEnabled(true);
		//Go through the root's children
		for(DXSSpatial s : root.getChildren()){
			//If we hit a node, we need to go through it and it's children
			if(s instanceof DXSNode){
				convertChildren((DXSNode)s, rootNode);
				//We hit a mesh and need to convert it
			} else if(s instanceof DXSPrimitive){
				rootNode.attachChild(convertDXSPrimitive((DXSPrimitive)s, PolygonWind.CounterClockWise));
				//We hit a light and must attach it to the root
			} else if(s instanceof DXSLight){
				//By default, the first 8 lights in the file are used
				state.attach(convertDXSLight((DXSLight)s));
			}
		}
		//Finally set the scale for this data
		rootNode.setLocalScale(scale);
		rootNode.setRenderState(state);
		rootNode.updateRenderState();
	}
	
	/**
	 * Converts and recursively goes through the dxs node's children.
	 * 
	 * @param n
	 * 		Dxs node to be converted
	 * @param parent
	 * 		Jme node that should be the parent of this node
	 */
	public void convertChildren(DXSNode n, Node parent){
		//Convert the given dxs node
		Node child = convertDXSNode(n);
		//Now convert the children
		for(DXSSpatial s : n.getChildren()){
			//If its a node, convert it and go through its children
			if(s instanceof DXSNode){
				convertChildren((DXSNode)s, child);
				//Otherwise if its a primitive, convert and attach
			} else if(s instanceof DXSPrimitive){
				child.attachChild(convertDXSPrimitive((DXSPrimitive)s,PolygonWind.CounterClockWise));
			}
		}
		//Once we're done, attach to the parent node
		parent.attachChild(child);
	}
	
	@Override
	public void setScale(float scale) {
		this.scale = scale;
	}

	@Override
	public void setTextureDirectory(URL dir) {
		texDir = dir;
	}
	
	/**
	 * Simply creates a jme Node with the name from the dxs node.
	 * 
	 * @param n
	 * 		DXS node
	 * @return
	 * 		JME node
	 */
	public final Node convertDXSNode(DXSNode n){
		Node node = new Node(n.getName());
		node.setLocalTranslation(convertDXSVector3f(n.getLocation()));
		node.setLocalScale(scale);
		//Vector3f v1 = node.getLocalRotation().getRotationColumn(2);
		//Vector3f v2 = convertDXSVector3f(n.getDirection());
		//node.getLocalRotation().fromAngleNormalAxis(v1.angleBetween(v2), v1.cross(v2));
		return node;
	}
	
	/**
	 * Converts a DXS primitive to the equivalent TriMesh. An optional
	 * parameter is the specification of vertex order (winding). By default
	 * it is CounterClockWise for a DXS primitive.
	 * 
	 * @param prim
	 * 		DXSPrimitive
	 * @param wind
	 * 		Optional winding (either PolgonWind.CounterClockWise or ClockWise)
	 * @return
	 * 		TriMesh
	 */
	public TriMesh convertDXSPrimitive(DXSPrimitive prim, PolygonWind wind){
		//Set default winding
		if(wind == null){
			wind = PolygonWind.CounterClockWise;
		}
		
		//Create a new TriMesh
		TriMesh mesh = new TriMesh(prim.getName());
		
		//Construct 3D information
		//TODO: We should probably have a color buffer, since in DeleD we can
		//specify vertex colors (but we don't parse them yet)
		mesh.reconstruct(prim.createVertBuffer(wind), prim.createNormalBuffer(wind),
				null,new TexCoords(prim.createTexBuffer(wind),2), prim.createIndexBuffer());
		
		//TODO: Add in a tag that we can specify a model bound in deled,
		//have default be the normal bounding box
		mesh.setModelBound(new BoundingBox());
		mesh.updateModelBound();
		
		//Apply render states
		RenderState[] states = convertDXSMaterial(prim.material);
		for(RenderState r : states){
			if(r != null)
				mesh.setRenderState(r);
		}
		
		//Update render states
		mesh.updateRenderState();
		mesh.setLocalScale(scale);
		return mesh;
	}
	
	/**
	 * Converts a DXS material to a number of jme render states.
	 * Every mesh can have a texture state (with multiple layers),
	 * a material state, and a blend state depending on the dxs layer
	 * information. If a state is not used, it will return as null.
	 * 
	 * Order of states will be returned as such:
	 * 
	 * TextureState, MaterialState, BlendState
	 * 
	 * @param mat
	 * 		DXSMaterial
	 * @return
	 * 		An array containing a TextureState, MaterialState,and BlendState
	 */
	public RenderState[] convertDXSMaterial(DXSMaterial mat){
		//Our render states, only one will be created
		//so if we have 3 color layers, only the first one will be used
		TextureState ts = null;
		MaterialState ms = null;
		BlendState bs = null;
		
		//For if we have multiple texture layers
		int indx = 0;
		
		for(DXSLayer layer : mat.layers){
			if(layer.material == DXSLayer.MaterialType.Color){
				//Only create a new material state if we don't already have one
				if(ms == null){
					//Set the diffuse color
					ms = display.getRenderer().createMaterialState();
					ms.setDiffuse(convertDXSColorRGBA(layer.color));
					//If we have blending, apply here
					if(layer.blend == DXSLayer.BlendType.Modulate ||
							layer.blend == DXSLayer.BlendType.Alphablend){
						//Only have one blendstate created
						if(bs == null){
							bs = display.getRenderer().createBlendState();
							bs.setBlendEnabled(true);
							bs.setSourceFunction(BlendState.SourceFunction.SourceAlpha);
							bs.setDestinationFunction(BlendState.DestinationFunction.One);
							bs.setTestEnabled(true);
							bs.setTestFunction(BlendState.TestFunction.GreaterThan);
						}
					} else if(layer.blend == DXSLayer.BlendType.Add){
						//TODO: Need to look into this, and also
						//see how to do modulate
					}
				}
			} else if(layer.material == DXSLayer.MaterialType.Texture){
				//Create the texture state if we need to
				if(ts == null){
					ts = display.getRenderer().createTextureState();
				}
				//Try loading the texture
				try {
					Texture tex = TextureManager.loadTexture(new URL(texDir,layer.texName),
								Texture.MinificationFilter.Trilinear,
								Texture.MagnificationFilter.Bilinear);
					ts.setTexture(tex,indx);
				} catch (MalformedURLException e) {
					System.out.println("Error loading texture - " + layer.texName);
				}
				indx++;
			}
		}
		//Save and return the states
		RenderState[] states = {ts, ms, bs};
		return states;
	}
	
	/**
	 * Conversion for a DXS light (point,spot, or directional) to
	 * the equivalent JME point, spot, or directional light.
	 * 
	 * @param l
	 * 		A light that subclasses DXSLight
	 * @return
	 * 		equivalent jme Light, otherwise null
	 */
	public Light convertDXSLight(DXSLight l){
		Light light = null;
		if(l instanceof DXSSpotLight){
			light = new SpotLight();
			
			//Set colors
			light.setAmbient(convertDXSColorRGBA(l.getAmbient()));
			light.setDiffuse(convertDXSColorRGBA(l.getDiffuse()));
			light.setSpecular(convertDXSColorRGBA(l.getSpecular()));
			
			//Set values
			light.setConstant(l.getConstant());
			light.setLinear(l.getLinear());
			light.setQuadratic(l.getQuadratic());
			
			//Set true/false
			light.setAttenuate(l.isAttenuate());
			light.setEnabled(l.isEnabled());
			light.setShadowCaster(l.isEnabled());
			
			//Set specific
			Vector3f v = convertDXSVector3f(((DXSSpotLight)l).getLocation());
			//Keep the scale in mind...
			v.multLocal(scale);
			((SpotLight)light).setLocation(v);
			((SpotLight)light).setDirection(convertDXSVector3f(((DXSSpotLight)l).getDirection()));
			
			//Is this actually the exponent? (we parse the "falloff" in the file)
			((SpotLight)light).setExponent(((DXSSpotLight)l).getExponent());
			
			//Outer or inner angle?
			((SpotLight)light).setAngle(((DXSSpotLight)l).getOuterAngle());
			
		} else if(l instanceof DXSPointLight){
			light = new PointLight();
			
			//Set colors
			light.setAmbient(convertDXSColorRGBA(l.getAmbient()));
			light.setDiffuse(convertDXSColorRGBA(l.getDiffuse()));
			light.setSpecular(convertDXSColorRGBA(l.getSpecular()));
			
			//Set values
			light.setConstant(l.getConstant());
			light.setLinear(l.getLinear());
			light.setQuadratic(l.getQuadratic());
			
			//Set true/false
			light.setAttenuate(l.isAttenuate());
			light.setEnabled(l.isEnabled());
			light.setShadowCaster(l.isEnabled());
			
			//Set specific
			Vector3f v = convertDXSVector3f(((DXSPointLight)l).getLocation());
			//Keep the scale in mind...
			v.multLocal(scale);
			((PointLight)light).setLocation(v);
		} else if(l instanceof DXSDirectionalLight){
			light = new DirectionalLight();
			
			//Set colors
			light.setAmbient(convertDXSColorRGBA(l.getAmbient()));
			light.setDiffuse(convertDXSColorRGBA(l.getDiffuse()));
			light.setSpecular(convertDXSColorRGBA(l.getSpecular()));
			
			//Set true/false
			light.setAttenuate(l.isAttenuate());
			light.setEnabled(l.isEnabled());
			light.setShadowCaster(l.isEnabled());
			
			//Set values
			light.setConstant(l.getConstant());
			light.setLinear(l.getLinear());
			light.setQuadratic(l.getQuadratic());
			
			//Set specific
			((DirectionalLight)light).setDirection(convertDXSVector3f(((DXSDirectionalLight)l).getDirection()));
		}
		return light;
	}
	
	/**
	 * Conversion for a DXS color to a JME color
	 * 
	 * @param c
	 * 		DXS colorRGBA
	 * @return
	 * 		JME colorRGBA
	 */
	public final ColorRGBA convertDXSColorRGBA(DXSColorRGBA c){
		return new ColorRGBA(c.r,c.g,c.b,c.a);
	}
	
	/**
	 * Conversion for a DXS Vector3f to a JME Vector3f
	 * 
	 * @param v DXS vector3f
	 * @return
	 * 		JME vector3f
	 */
	public final Vector3f convertDXSVector3f(DXSVector3f v){
		return new Vector3f(v.x,v.y,v.z);
	}

	@Override
	public void save(File dir) {
		try {
			String filename = rootNode.getName();
			filename = filename.substring(0,filename.lastIndexOf(".")) + ".jme";
			String path = dir.getPath();
			if(!path.endsWith(File.separator)){
				path += File.separator;
			}
			//Ensure directory is maded
			dir.mkdir();
			BinaryExporter.getInstance().save(rootNode, new File(path + filename));
		} catch (IOException e) {
			System.out.println("Model failed to be saved to file!");
		}
	}

	@Override
	public Node getData() {
		return rootNode;
	}

	@Override
	public void flush() {
		scale = 1.0f;
		texDir = null;
		rootNode = null;
	}

}
